
#ifndef ROPUFU_FORMAT_MATSTREAM_V4_HPP_INCLUDED
#define ROPUFU_FORMAT_MATSTREAM_V4_HPP_INCLUDED

#include "../distributed/matrix.hpp"
#include "matstream.hpp"

#include <array>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>

namespace ropufu
{
    namespace distributed
    {
        enum class mat4_data_format : int
        {
            ieee_little_endian = 0000,
            ieee_big_endian = 1000,
            vax_d_float = 2000,
            vax_g_float = 3000,
            cray = 4000
        };

        template <typename T>
        struct mat4_data_type_id;

        template <>
        struct mat4_data_type_id<double>
        {
            static const std::int32_t value = 00;
        };

        template <>
        struct mat4_data_type_id<float>
        {
            static const std::int32_t value = 10;
        };

        template <>
        struct mat4_data_type_id<std::int32_t>
        {
            static const std::int32_t value = 20;
        };

        template <>
        struct mat4_data_type_id<std::int16_t>
        {
            static const std::int32_t value = 30;
        };

        template <>
        struct mat4_data_type_id<std::uint16_t>
        {
            static const std::int32_t value = 40;
        };

        template <>
        struct mat4_data_type_id<std::uint8_t>
        {
            static const std::int32_t value = 50;
        };

        enum class mat4_matrix_type_id : int
        {
            full = 0,
            text = 1,
            sparse = 2
        };

        template <>
        struct matstream_header<4>
        {
            static const std::int32_t mat_level = 4;
            typedef matstream_header<4> type;

        private:
            std::int32_t m_data_format_id;
            std::int32_t m_data_type_id;
            std::int32_t m_matrix_type_id;

            std::int32_t m_global_height;
            std::int32_t m_global_width;
            std::int32_t m_is_complex = 0;

            std::string m_name;

            std::int32_t build_format_type_id() const
            {
                return this->m_data_format_id + this->m_data_type_id + this->m_matrix_type_id;
            }

            void decompose_format_type_id(std::int32_t format_type_id)
            {
                this->m_data_format_id = 1000 * (format_type_id / 1000);
                format_type_id -= this->m_data_format_id;

                this->m_data_type_id = 10 * (format_type_id / 10);
                format_type_id -= this->m_data_type_id;

                this->m_matrix_type_id = format_type_id;
            }

        public:
            std::size_t read(const std::string& filename)
            {
                std::size_t bytes_read = 0UL;
                std::ifstream filestream;

                filestream.open(filename.c_str(), std::ios::in | std::ios::binary);
                if (filestream.fail()) std::cerr << "Failed to open file: " << filename << std::endl;
                else
                {
                    std::int32_t format_type_id;
                    std::int32_t name_length;
                    char terminator;

                    filestream.seekg(0);
                    // Fixed-size header.
                    filestream.read(reinterpret_cast<char*>(&format_type_id),        sizeof(std::int32_t)); // type
                    filestream.read(reinterpret_cast<char*>(&this->m_global_height), sizeof(std::int32_t)); // mrows
                    filestream.read(reinterpret_cast<char*>(&this->m_global_width),  sizeof(std::int32_t)); // ncols
                    filestream.read(reinterpret_cast<char*>(&this->m_is_complex),    sizeof(std::int32_t)); // imagf
                    filestream.read(reinterpret_cast<char*>(&name_length),           sizeof(std::int32_t)); // namlen

                    // Name.
                    std::vector<char> text_data(name_length - 1);
                    filestream.read(reinterpret_cast<char*>(&text_data[0]), name_length - 1);
                    filestream.read(&terminator, 1);
                    filestream.close();

                    this->decompose_format_type_id(format_type_id);
                    this->m_name = std::string(std::begin(text_data), std::end(text_data));
                    bytes_read = this->size();
                }

                return bytes_read;
            }

            std::size_t write(const std::string& filename)
            {
                std::size_t position = 0UL;
                std::size_t existing_size = 0UL;
                std::ofstream filestream;

                filestream.open(filename.c_str(), std::ios::in | std::ios::out | std::ios::binary);
                if (filestream.fail()) std::cerr << "Failed to open file: " << filename << std::endl;
                else
                {
                    std::int32_t format_type_id = this->build_format_type_id();
                    std::int32_t name_length = static_cast<std::int32_t>(this->m_name.size() + 1UL);
                    char terminator = '\0';

                    filestream.seekp(0, std::ios::end);
                    existing_size = filestream.tellp();
                    // Fixed-size header.
                    filestream.write(reinterpret_cast<char*>(&format_type_id),        sizeof(std::int32_t)); // type
                    filestream.write(reinterpret_cast<char*>(&this->m_global_height), sizeof(std::int32_t)); // mrows
                    filestream.write(reinterpret_cast<char*>(&this->m_global_width),  sizeof(std::int32_t)); // ncols
                    filestream.write(reinterpret_cast<char*>(&this->m_is_complex),    sizeof(std::int32_t)); // imagf
                    filestream.write(reinterpret_cast<char*>(&name_length),           sizeof(std::int32_t)); // namlen

                    // Name.
                    filestream.write(this->m_name.c_str(), name_length - 1);
                    filestream.write(&terminator, 1);
                    filestream.close();

                    position = existing_size + this->size();
                }

                return position;
            }

            template <typename T>
            void initialize(const matrix<T>& mat,
                mat4_data_format data_format = mat4_data_format::ieee_little_endian, mat4_matrix_type_id matrix_type_id = mat4_matrix_type_id::full)
            {
                this->m_data_format_id = static_cast<std::int32_t>(data_format);
                this->m_data_type_id = mat4_data_type_id<T>::value;
                this->m_matrix_type_id = static_cast<std::int32_t>(matrix_type_id);

                this->m_global_height = static_cast<std::int32_t>(mat.get_global_height());
                this->m_global_width = static_cast<std::int32_t>(mat.get_global_width());
            }

            void name(const std::ostringstream& name_stream)
            {
                this->m_name = name_stream.str();
            }

            void name(const std::string& name)
            {
                this->m_name = name;
            }

            void name(std::string&& name)
            {
                this->m_name = name;
            }

            std::size_t size() const
            {
                return 5 * sizeof(std::int32_t) + this->m_name.size() + 1;
            }
        };

        /** A MAT-file may contain one or more matrices. The matrices are written
        * sequentially on disk, with the bytes forming a continuous stream. Each
        * matrix starts with a fixed-length 20-byte header that contains information
        * describing certain attributes of the Matrix. The 20-byte header consists of five
        * 4-byte integers:
        * 1) type (mat4_data_format + mat4_data_type_id::value + mat4_matrix_type_id);
        * 2) mrows (number of rows in the matrix);
        * 3) ncols (number of columns in the matrix);
        * 4) imagf (1 if the matrix has an imaginary part, 0 otherwise);
        * 5) namlen (length of the matrix name plus 1).
        *
        * Immediately following the fixed length header is the data whose length is
        * dependent on the variables in the fixed length header:
        * 1) name : <namelen> bytes.
        *    ASCII bytes, the last one of which must be a null character '\0';
        * 2) real : <mrows> * <ncols> numbers, size depends on <mat4_data_type>.
        *    Real part of the matrix, with data stored column-wise.
        *    For example, a matrix
        *    || a b c ||
        *    || d e f ||
        *    would be written as "adbecf".
        * 3) imag : <imagf> * <mrows> * <ncols> numbers, size depends on <mat4_data_type>.
        *    Imaginary part of the matrix, with data stored column-wise.
        */
        template <typename TGrid>
        struct matstream<4, TGrid>
        {
            typedef TGrid grid_type;
            typedef matstream<4, grid_type> type;
            typedef matstream_header<4> header_type;
            static const std::int32_t mat_level = 4;
            //static const std::int32_t header_size = 5 * sizeof(std::int32_t); // 20 bytes.

        private:
            std::string m_filename;
            std::size_t m_file_position;
            std::ostringstream m_namestream;

        public:
            matstream(const std::string& filename)
                : m_filename(filename), m_file_position(0UL), m_namestream()
            {

            }

            void clear()
            {
                grid_type grid;
                if (grid.is_master())
                {
                    std::ofstream filestream;
                    filestream.open(this->m_filename.c_str(), std::ios::in | std::ios::out | std::ios::binary | std::ios::trunc);
                    if (filestream.fail()) std::cerr << "Failed to create file: " << this->m_filename << std::endl;
                    else filestream.close();
                }
            }

            type& operator <<(const std::string& name)
            {
                this->m_namestream << name;
                return *this;
            }

            type& operator <<(const std::int32_t& name)
            {
                this->m_namestream << name;
                return *this;
            }

            type& operator <<(const std::size_t& name)
            {
                this->m_namestream << name;
                return *this;
            }

            template <typename T>
            type& operator <<(const matrix<T>& mat)
            {
                typedef T data_type;
                grid_type grid;
                std::ofstream filestream;

                // Initialize header.
                header_type header;
                header.initialize(mat);
                header.name(this->m_namestream);
                // Reset name stream.
                this->m_namestream.clear();
                this->m_namestream.str("");
                
                std::int32_t position = 0;
                if (grid.is_master())
                {
                    position = static_cast<std::int32_t>(header.write(this->m_filename));
                    // TODO: broadcast position.
                }
                grid.barrier();

                // Write body.
                filestream.open(this->m_filename.c_str(), std::ios::in | std::ios::out | std::ios::binary);
                if (filestream.fail()) std::cerr << "Failed to open file: " << this->m_filename << std::endl;
                else
                {
                    std::int32_t m = static_cast<std::int32_t>(mat.get_local_height());
                    std::int32_t n = static_cast<std::int32_t>(mat.get_local_width());
                    std::int32_t global_height = static_cast<std::int32_t>(mat.get_global_height());

                    for (std::int32_t i = 0; i < m; i++)
                    {
                        std::int32_t i_global = grid.row_local_to_global(i);
                        for (std::int32_t j = 0; j < n; j++)
                        {
                            std::int32_t j_global = grid.column_local_to_global(j);
                            filestream.seekp(position + (j_global * global_height + i_global) * sizeof(data_type));
                            filestream.write(reinterpret_cast<const char*>(&mat.read(i, j)), sizeof(data_type));
                        }
                    }
                    filestream.close();
                }
                grid.barrier();

                return *this;
            }

            ///** Loads a matrix from a file. */
            //template <typename T>
            //static matrix<T> load(const std::string& filename)
            //{
            //    typedef T data_type;
            //    grid_type grid;
            //    std::string text;
            //    std::int32_t type_id;
            //    std::int32_t m, n; // Global size.

            //    std::int32_t header_size = type::read_header(filename, text, type_id, m, n);
            //    grid.barrier();
            //    if (mat4_data_type_id<data_type>::value != type_id) std::cerr << "Matrix data type mismatch in: " << filename << std::endl;

            //    matrix<data_type> mat(m, n);
            //    type::load_unchecked(filename, header_size, mat);
            //    return mat;
            //}

            ///** Loads a matrix from a file into an existing variable <mat>. */
            //template <typename T>
            //static void load(const std::string& filename, matrix<T>& mat)
            //{
            //    typedef T data_type;
            //    std::string text;
            //    std::int32_t type_id;
            //    std::int32_t m, n; // Global size.

            //    std::int32_t header_size = type::read_header(filename, text, type_id, m, n);
            //    grid.barrier();
            //    if (mat4_data_type_id<data_type>::value != type_id) std::cerr << "Matrix data type mismatch in: " << filename << std::endl;

            //    // Resize check.
            //    if (mat.get_global_height() != m || mat.get_global_width() != n)
            //    {
            //        matrix<data_type> proper(m, n);
            //        mat.steal(proper);
            //    }
            //    type::load_unchecked(filename, header_size, mat);
            //}

        private:
            //template <typename T>
            //static void load_unchecked(const std::string& filename, std::int32_t header_size, matrix<T>& mat)
            //{
            //    typedef T data_type;
            //    grid_type grid;
            //    std::ifstream filestream;

            //    filestream.open(filename.c_str(), std::ios::in | std::ios::binary);
            //    if (filestream.fail()) std::cerr << "Failed to open file: " << filename << std::endl;
            //    else
            //    {
            //        std::int32_t local_height = mat.get_local_height();
            //        std::int32_t local_width = mat.get_local_width();
            //        std::int32_t global_height = mat.get_global_height();

            //        data_type current_value;

            //        for (std::int32_t i = 0; i < local_height; i++)
            //        {
            //            std::int32_t i_global = grid.row_local_to_global(i);
            //            for (std::int32_t j = 0; j < local_width; j++)
            //            {
            //                std::int32_t j_global = grid.column_local_to_global(j);
            //                filestream.seekg(header_size + (j_global * global_height + i_global) * sizeof(data_type));
            //                filestream.read(reinterpret_cast<char*>(&current_value), sizeof(data_type));
            //                mat.write(i, j, current_value);
            //            }
            //        }
            //        filestream.close();
            //    }
            //    grid.barrier();
            //}
        };
    }
}

#endif // ROPUFU_FORMAT_MATSTREAM_V4_HPP_INCLUDED
